#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <unistd.h>
#include <limits.h>
#include <time.h>
#include <math.h>
#include "util.h"

/* CONSTANTS */
#define WINDOW_TITLE "Test 4 - Music Visualizer"
#define DEFAULT_SONG "song.mp3"
/* Command line flags defined here */
#define NUMFLAGS 2
static const char flags[NUMFLAGS] = {'d', 'h'};
static int flagsfound[NUMFLAGS] = {0, 0};

#define PI 3.14159265
/* Default window size */
#define WINDOW_WIDTH 500
#define WINDOW_HEIGHT 500
/* Min and max scale */
#define MIN_SCALE 0.3
#define MAX_SCALE 2.0
/* Initial camera settings */
#define DEFAULT_ORTHO_HALFHEIGHT 120.0
#define DEFAULT_ORTHO_HALFWIDTH 100.0
#define DEFAULT_ORTHO_HALFZ 250.0
#define CAMERA_X 100
#define CAMERA_Y 200
#define CAMERA_Z 500
#define MIN_ZOOM 0.5
#define MAX_ZOOM 2
#define UP_VECTOR_X 0.0
#define UP_VECTOR_Y 1.0
#define UP_VECTOR_Z 0.0

#define SPECLEN 256
#define MAX_NUM_SPHERES 256
#define SPHERE_POLYGONS 50
#define INIT_NUM_SPHERES 5
#define SPH_ANGLE_STEP 1
#define TRAJECTORY_RADIUS_MIN 3
#define TRAJECTORY_RADIUS_COEFF 400
#define TRAJECTORY_RADIUS_RAND 7
#define TRAJECTORY_SLOPE_RANGE 40
#define TIME_DELTA 0.025

#define FULLSCREEN 1
/* End constant declaration */

using namespace FMOD;
using namespace std;

System  *fmodsys;
Sound   *fmodsnd;
Channel *fmodchn;

float *specL, *specR;


int
    isDebug
/* End of command line flags */
/* Now variables */
  , window_width = WINDOW_WIDTH
  , window_height = WINDOW_HEIGHT
  , lastX = 0, deltaDragX = 0
  , dragKey
  , lastY, scaleKey
  , rotateX, rotateKey
  , zoomKey, moveKey
  , rotateCameraKey
  , renderKey = 'W'
  , rollKey
  ;
bool 
      redraw = false
    , drawAxes = false
    , drawPlanes = false
    , drawPerspective = true
    , dragObject = false
    , scaleObject = false
    , rotateObject = false
    , zoomCamera = false
    , moveCamera = false
    , rotateCamera = false
    , rollCamera = false
    , soundPaused = false
    ;
double
/* scale data */
      scale = 1.0, initScale = 1.0
    , rescale = 1.0
    , aspectratio = 1.0
/* zoom data */
    , zoom = 1.0
/* rotation angle */
    , angle
/* camera position, before zooming */
    , cameraX = CAMERA_X
    , cameraY = CAMERA_Y
    , cameraZ = CAMERA_Z
/* camera movement, horizontal and vertical */
    , moveCameraX = 0, moveCameraY = 0
/* camera rotation, horizontal and vertical */
    , rotateCameraX = 0, rotateCameraY = 0
/* up vector and rolling */
    , upVectorX = UP_VECTOR_X
    , upVectorY = UP_VECTOR_Y
    , upVectorZ = UP_VECTOR_Z
    , rollAlpha = PI / 2
    , wth = 1
    , start = -SPECLEN
    , end = SPECLEN
    , weight = 500
    , r, phi, theta
    , radius
    , step = SPH_ANGLE_STEP
    , global_time = 0.0
    , sph_boundary[3][2] = {
        {-100.0, 100.0},
        {-100.0, 100.0},
        {-100.0, 100.0}
    }
    , sph_target[MAX_NUM_SPHERES][3]
    , sph_slope[MAX_NUM_SPHERES][3]
    , sph_current[MAX_NUM_SPHERES][3]
    , sph_color[MAX_NUM_SPHERES][3]
    , sph_coeff[MAX_NUM_SPHERES][3][4]
    ;

GLfloat 
        mat_shininess[] = { 500.0 }
      , mat_specular[] = { 0.5, 0.5, 0.9, 1.0 }
      , light_position[] = { 1.0, 1.0, 1.0, 0.0 }
      , sph_material[4]
      ;

struct GLpoint {
   GLfloat x, y, z;
};

GLfloat
      rotation[16] = {
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    }
    , initRotation[16] = {
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    }
    ;

/* Forward declarations */
void initSpheres();
void generateTrajectory(int i);
void moveSphere(int i);
void dumpSphereTarget(int i);
void dumpSpherePos(int i);


/* Display function
 * Called anytime the display needs to be updated
 */
void display(void)
{
    global_time += TIME_DELTA;
    if (global_time >= 1) {
        global_time = 0;
        //dumpSpherePos(0);
        for (int j=0; j<INIT_NUM_SPHERES; j++) {
            generateTrajectory(j);
        }
        //dumpSphereTarget(0);
    }
    bool playing;
    fmodchn->getPaused( &playing );
    if ( !playing ) {
        fmodchn->getSpectrum( specL, SPECLEN, 0, FMOD_DSP_FFT_WINDOW_BLACKMAN ); 
        fmodchn->getSpectrum( specR, SPECLEN, 1, FMOD_DSP_FFT_WINDOW_BLACKMAN ); 
    }
    else {
        memset( specL, 0, SPECLEN*sizeof(float) );
        memset( specR, 0, SPECLEN*sizeof(float) ); 
    }

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    if (drawPerspective) {
        gluPerspective(36.0, aspectratio, 20.0, 1000.0);
        //glFrustum(-10.0, 10.0, -10.0, 10.0, 20.0, 1000.0);
        gluLookAt((cameraX+moveCameraX)*zoom
            , (cameraY+moveCameraY)*zoom
            , cameraZ*zoom
            , moveCameraX*zoom
            , moveCameraY*zoom
            , 0.0
            , upVectorX
            , upVectorY
            , upVectorZ
        );
    }
    else {
        /* preserve scale */
        glOrtho(-window_width/WINDOW_WIDTH * DEFAULT_ORTHO_HALFWIDTH, window_width/WINDOW_WIDTH * DEFAULT_ORTHO_HALFWIDTH, -window_height/WINDOW_HEIGHT * DEFAULT_ORTHO_HALFHEIGHT, window_height/WINDOW_HEIGHT * DEFAULT_ORTHO_HALFHEIGHT,
            -DEFAULT_ORTHO_HALFZ, DEFAULT_ORTHO_HALFZ);
    }
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    /* draw axes */
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);
    if (drawAxes) {
        glColor3f(1.0, 0.0, 0.0);
        glBegin(GL_LINES);
            glVertex3f(0.0,0.0,0.0);
            glVertex3f(50.0,0.0,0.0);
        glEnd();
        glColor3f(0.0, 1.0, 0.0);
        glBegin(GL_LINES);
            glVertex3f(0.0,0.0,0.0);
            glVertex3f(0.0,50.0,0.0);
        glEnd();
        glColor3f(0.0, 0.0, 1.0);
        glBegin(GL_LINES);
            glVertex3f(0.0,0.0,0.0);
            glVertex3f(0.0,0.0,50.0);
        glEnd();
    }
    /* draw planes */
    if (drawPlanes) {
        /* XY plane */
        glColor3f(0.8, 0.8, 1.0);
        if (renderKey == 'W') glBegin(GL_LINE_LOOP);
        else glBegin(GL_POLYGON);
            glVertex3f(-50.0,-50.0,0.0);
            glVertex3f(-50.0,50.0,0.0);
            glVertex3f(50.0,50.0,0.0);
            glVertex3f(50.0,-50.0,0.0);
        glEnd();
        /* XZ plane */
        glColor3f(0.8, 1.0, 0.8);
        if (renderKey == 'W') glBegin(GL_LINE_LOOP);
        else glBegin(GL_POLYGON);
            glVertex3f(-50.0,0.0,-50.0);
            glVertex3f(-50.0,0.0,50.0);
            glVertex3f(50.0,0.0,50.0);
            glVertex3f(50.0,0.0,-50.0);
        glEnd();
        /* YZ plane */
        glColor3f(1.0, 0.8, 0.8);
        if (renderKey == 'W') glBegin(GL_LINE_LOOP);
        else glBegin(GL_POLYGON);
            glVertex3f(0.0,-50.0,-50.0);
            glVertex3f(0.0,50.0,-50.0);
            glVertex3f(0.0,50.0,50.0);
            glVertex3f(0.0,-50.0,50.0);
        glEnd();
    }
    glLoadIdentity();
    /* Scale for rotation */
    if (dragObject) {
        switch (dragKey) {
            case 'x':
                glTranslatef(deltaDragX, 0, 0);
                break;
            case 'y':
                glTranslatef(0, deltaDragX, 0);
                break;
            case 'z':
                glTranslatef(0, 0, deltaDragX);
                break;
        }
    }
    /* add the previous state */
    glMultMatrixf(rotation);
    if (rotateObject) {
        switch (rotateKey) {
            case 'a':
                glRotatef(angle, 1.0, 0.0, 0.0);
                break;
            case 'b':
                glRotatef(angle, 0.0, 1.0, 0.0);
                break;
            case 'c':
                glRotatef(angle, 0.0, 0.0, 1.0);
                break;
        }
        angle = 0;
    }
    /* save new matrix */
    glGetFloatv(GL_MODELVIEW_MATRIX, rotation);
    /* scaling not stacked, so do after */
    glScalef(scale*rescale, scale*rescale, scale*rescale);
    //GLUquadric *myQuad;
    //myQuad=gluNewQuadric();
    static GLint slices, stacks;
    slices = stacks = SPHERE_POLYGONS;
    radius = 5.0 + 5*specR[0] + 4*specL[0];
    for (int j=0; j<INIT_NUM_SPHERES; j++) {
        glPushMatrix();
            glColor4f( sph_color[j][0]-10*specL[j], sph_color[j][1]-5*specR[j], sph_color[j][2]+6*specL[j], 0.6 );
            sph_material[0] = sph_color[j][0]-specL[j];
            sph_material[1] = sph_color[j][1]-specR[j];
            sph_material[2] = sph_color[j][2]+specL[j];
            sph_material[3] = 0.6;
            light_position[0] = sph_current[0][0];
            light_position[1] = sph_current[0][1];
            light_position[2] = sph_current[0][2];
            //light_position[3] = 1.0 + specL[j];
            int light_num = 0;
            switch (j) {
                case 1:
                    light_num = GL_LIGHT1;
                    break;
                case 2:
                    light_num = GL_LIGHT2;
                    break;
                case 3:
                    light_num = GL_LIGHT3;
                    break;
                case 4:
                    light_num = GL_LIGHT4;
                    break;
                default:
                case 0:
                    light_num = GL_LIGHT0;
                    break;
            }
            glLightfv(light_num, GL_POSITION, light_position);
            glMaterialfv(GL_FRONT, GL_DIFFUSE, sph_material);
            glTranslatef(sph_current[j][0], sph_current[j][1], sph_current[j][2]);
            //gluSphere( myQuad , radius , slices , stacks  );
            glutSolidSphere( radius , slices , stacks  );
        glPopMatrix();
        moveSphere(j);
    }
    /*double tmp, tmp2;
    int i,j;
    for (j=0; j<SPECLEN; j+=1) {
        i = SPECLEN-1 - j;
        //fprintf(stderr, "%f ", specL[i]);
        tmp = weight*specL[i];
        tmp2 = tmp/2+2;

        glBegin(GL_POLYGON);
            glColor4f( 0.6-10*specL[i], 0.2-5*specL[i], 0.5+6*specL[i], 0.4 );
            glVertex3f( start+wth*j, tmp2, 0.0 );
            glVertex3f( start+wth*j+wth, tmp2, 0.0 );
            glVertex3f( start+wth*j+wth, -tmp+tmp2, 0.0 );
            glVertex3f( start+wth*j, -tmp+tmp2, 0.0 );
        glEnd();

        tmp = weight*specR[i];
        tmp2 = tmp/2+2;

        glBegin(GL_POLYGON);
            glColor4f( 0.6-10*specR[i], 0.2-5*specR[i], 0.5+6*specR[i], 0.4 );
            glVertex3f( end-wth*j, tmp2, 0.0 );
            glVertex3f( end-wth*j+wth, tmp2, 0.0 );
            glVertex3f( end-+wth*j+wth, -tmp+tmp2, 0.0 );
            glVertex3f( end-wth*j, -tmp+tmp2, 0.0 );
        glEnd();
    }
    //fprintf(stderr, "\n");*/
    glutSwapBuffers();
}

/* Takes action when a keyboard key is pressed
 * Quits on escape. Handles world-space and
 * object-space rotation.
 * Resets on 'r'
 * And deletes on "Delete"
 */
void keyboardAction(unsigned char key, int x, int y)
{
    if (dragObject || rotateObject || scaleObject) return;
    switch(key) {
        case 'Q':
            cameraX = CAMERA_X; cameraY = CAMERA_Y; cameraZ = CAMERA_Z;
            zoom = 1.0; moveCameraX = 0; moveCameraY = 0;
            upVectorX = UP_VECTOR_X; upVectorY = UP_VECTOR_Y; upVectorZ = UP_VECTOR_Z;
            for (int i = 0; i<16; i++) {
                rotation[i] = initRotation[i];
            }
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glutPostRedisplay();
            break;
        case 'r':
            scale = initScale; rescale = 1.0; zoom = 1.0;
            for (int i = 0; i<16; i++) {
                rotation[i] = initRotation[i];
            }
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glutPostRedisplay();
            break;
        case 27:
            if (isDebug) {
                fprintf(stderr, "Exiting program, ESC key pressed.\n");
            }
            exit(0);
        case 'w':
            drawAxes = 1 - drawAxes;
            glutPostRedisplay();
            break;
        case 'e':
            drawPlanes = 1 - drawPlanes;
            glutPostRedisplay();
            break;
        case 'p':
            drawPerspective = 1 - drawPerspective;
            glutPostRedisplay();
            break;
        case 'x':
        case 'y':
        case 'z':
            dragKey = key;
            break;
        case 'a':
        case 'b':
        case 'c':
            rotateKey = key;
            break;
        case 's':
            scaleKey = key;
            break;
        case 'M':
            zoomKey = key;
            break;
        case 'H':
        case 'V':
            moveKey = key;
            break;
        case 'P':
        case 'Y':
            rotateCameraKey = key;
            break;
        case 'W':
        case 'C':
        case 'G':
            renderKey = key;
            glutPostRedisplay();
            break;
        case 'R':
            rollKey = key;
            break;
        case ' ':
            soundPaused = !soundPaused;
            fmodchn->setPaused(soundPaused);
            break;
        default:
            fprintf(stderr, "Unreckognized key: %d\n", key);
    }
}

/* resets keys; used in conjunction with
 * glutSetKeyRepeat(GLUT_KEY_REPEAT_OFF) -- see main()
 */
void keyboardUp(unsigned char key, int x, int y) {
    rotateKey = 0;
    scaleKey = 0;
    dragKey = 0;
    zoomKey = 0;
    moveKey = 0;
    rotateCameraKey = 0;
    rollKey = 0;
}

/* Get the flags set from command line and set
 * corresponding variables.
 */
int getAndSetFlags(int argc, char** argv) {
    int i, j;
    if (argc >= 2) {
        for (j = 0; j < NUMFLAGS; j++) {
            for (i = 1; i < argc; i++) {
                if (!flagsfound[j]
                    && strchr(argv[i], '-') 
                    && strchr(argv[i], flags[j])) {
                    flagsfound[j] = 1;
                    if (flags[j] == 'd') {
                        isDebug = 1;
                        argc--;
                    }
                    else if (flags[j] == 'h') {
    /* --------------- HELP SECTION --------------- */
    printf("Usage: %s [-d(debug)|-h(help)] <musicfile.[mp3|flac]>\n", argv[0]);
    printf("Instructions (CaSe SeNsItIvE!):\n");
    printf("- r -- resets model manipulation\n");
    printf("- Q -- resets camera manipulation\n");
    printf("MODEL MANIPULATION:\n");
    printf("- press and hold s, then drag *middle* mouse *vertically* to scale in *object* space\n");
    printf("- press and hold a, b, or c, then drag *left* mouse *horizontally* to rotate in *object* space around x, y and z axes respectively\n");
    printf("- press and hold x, y, or z, then drag *right* mouse *horizontally* to translate in *world* space along x, y and z axes respectively\n");
    printf("CAMERA MANIPULATION:\n");
    printf("- press and hold M, then drag *middle* mouse *vertically* to zoom camera\n");
    printf("- press and hold H or V, then drag *right* mouse *horizontally* to move camera horizontally or vertically\n");
    printf("- press and hold R, P or Y, then drag *left* mouse *horizontally* to do a roll, pitch or yaw of the camera\n");
    printf("RENDERING MODES / PERSPECTIVE:\n");
    printf("- press p to *toggle* perspective or parallel projection\n");
    /* --------------- /HELP SECTION --------------- */
                        exit(0);
                    }
                }
            }
        }
    }
    return argc;
}

/* Takes action when the mouse is clicked.
 * Actions include: dragging, rotation, scaling
 * Setting flag for deletion (when ctrl+click is pressed)
 */
void mouseClick(int button, int state, int x, int y) {
    if (state == GLUT_DOWN) {
        if (rotateKey &&
            /* a rotate key has been pressed */
            (button == GLUT_LEFT_BUTTON)) {
                /* start rotation mode */
            switch (rotateKey) {
                case 'a':
                case 'b':
                case 'c':
                    rotateObject = true;
                    break;
            }
        }
        /* dragging is right button */
        else if (dragKey && button == GLUT_RIGHT_BUTTON) {
            dragObject = true;
            lastX = x - window_width/2;
            deltaDragX = 0;
        }
        /* scaling is middle button */
        else if ((scaleKey) && button == GLUT_MIDDLE_BUTTON) {
            scaleObject = true;
            lastY = y;
        }
        else if ((zoomKey) && button == GLUT_MIDDLE_BUTTON) {
            zoomCamera = true;
            lastY = y;
        }
        else if ((moveKey) && button == GLUT_RIGHT_BUTTON) {
            moveCamera = true;
            lastX = x;
        }
        else if ((rotateCameraKey) && button == GLUT_LEFT_BUTTON) {
            rotateCamera = true;
            lastX = x;
        }
        else if ((rollKey) && button == GLUT_LEFT_BUTTON) {
            rollCamera = true;
            lastX = x;
        }
        return;
    }
    if (state == GLUT_UP) {
        /* unset all variables on mouseup */
        scaleObject = false;
        rotateObject = false;
        dragObject = false;
        zoomCamera = false;
        moveCamera = false;
        rotateCamera = false;
        rollCamera = false;
    }
}

/* initializes coordinates for rotating camera
 * used only for pitch and yaw
 * run once -- see main()
 */
void initRotateCoords() {
    r = sqrt(cameraX*cameraX
            + cameraY*cameraY
            + cameraZ*cameraZ);
    phi = acos(cameraY/r);
    double S = sqrt(cameraX*cameraX
            + cameraZ*cameraZ);
    theta = asin(cameraZ/S);
}

/* rotates camera in spherical/polar coordinates
 * see http://www.math.montana.edu/frankw/ccp/multiworld/multipleIVP/spherical/body.htm
 */
void doRotateCamera(float newrotate) {
    switch (rotateCameraKey) {
        case 'P':
            phi += newrotate;
            break;
        case 'Y':
            theta += newrotate;
            break;
    }
    double sinphi = sin(phi);
    if (sinphi < 0)
        sinphi = -sinphi;
    cameraX = r * sinphi * cos(theta);
    cameraZ = r * sinphi * sin(theta);
    cameraY = r * cos(phi);
}

/* rolls camera in 2D polar coordinates (z held constant)
 * upVector assumed already normalized
 */
void doRollCamera(float newroll) {
    rollAlpha += newroll;
    upVectorX = cos(rollAlpha);
    upVectorY = sin(rollAlpha);
}

/* This gets called when the mouse is pressed and moving */
void mouseMotion(int x, int y) {
    if (dragObject) {
        /* Dragging the object */
        deltaDragX = x - window_width/2 - lastX;
        lastX = x - window_width/2;
        glutPostRedisplay();
    }
    else if (scaleObject) {
        float newrescale = rescale
            + ((float)y-(float)lastY) / 200.0;
        lastY = y;
        if (newrescale > MIN_SCALE && newrescale < MAX_SCALE) {
            rescale = newrescale;
            glutPostRedisplay();
        }
    }
    else if (rotateObject) {
        if (x == rotateX) {
            angle = 0;
            return;
        }
        if (x < rotateX) {
            angle = -2;
        }
        else {
            angle = 2;
        }
        rotateX = x;
        if (isDebug) {
            if (angle > 0) {
                fprintf(stderr, "right\n");
            }
            else if (angle < 0) {
                fprintf(stderr, "left\n");
            }
            else {
                fprintf(stderr, "steady\n");
            }
        }
        glutPostRedisplay();
    }
    else if (zoomCamera) {
        float newzoom = zoom
            + ((float)y-(float)lastY) / 200.0;
        lastY = y;
        if (newzoom > MIN_ZOOM && newzoom < MAX_ZOOM) {
            zoom = newzoom;
            glutPostRedisplay();
        }
    }
    else if (moveCamera) {
        int newmove = x-lastX;
        lastX = x;
        switch (moveKey) {
            case 'H':
                moveCameraX += newmove;
                break;
            case 'V':
                moveCameraY += newmove;
                break;
        }
        glutPostRedisplay();
    }
    else if (rotateCamera) {
        float newrotate = x-lastX;
        lastX = x;
        if (newrotate > 0) {
            newrotate = PI / 90;
        }
        else {
            newrotate = - PI / 90;
        }
        doRotateCamera(newrotate);
        glutPostRedisplay();
    }
    else if (rollCamera) {
        float newroll = x-lastX;
        lastX = x;
        if (newroll > 0) {
            newroll = PI / 90;
        }
        else {
            newroll = - PI / 90;
        }
        doRollCamera(newroll);
        glutPostRedisplay();
    }
}

/* Reshape function for when window is reshaped */
void reshape(int x, int y) {
    window_width = x;
    window_height = y;
    aspectratio = (double)x/(double)y;
    glViewport (0, 0, (GLsizei) window_width, (GLsizei) window_height);
    redraw = true;
    /* Done, now just call display() */
    display();
}

void initFMOD(const char * path)
{
    System_Create(&fmodsys);
    fmodsys->init(1,FMOD_INIT_NORMAL,0);
    fmodsys->createSound(path,
                FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM,
                0, &fmodsnd);
    fmodsys->playSound(FMOD_CHANNEL_FREE,fmodsnd,true,&fmodchn);
    specL = (float*)malloc(SPECLEN*sizeof(float));
    specR = (float*)malloc(SPECLEN*sizeof(float));
    unsigned int len;
    fmodchn->setVolume(1);
    fmodsnd->getLength( &len, FMOD_TIMEUNIT_MS );
    fmodchn->setPosition( (int)(len*0.0), FMOD_TIMEUNIT_MS );
}

int main(int argc, char** argv)
{
    srand ( time(NULL) );
    int newargc = getAndSetFlags(argc, argv);
    if ( newargc<2 ) {
        initFMOD(DEFAULT_SONG);
    }
    else {
        initFMOD(argv[argc-1]);
    }

    /* Initialize glut */
    glutInit(&argc, argv);
    glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); 
    glutInitWindowSize (window_width, window_height);
    glutInitWindowPosition (100, 100);
    glutCreateWindow (WINDOW_TITLE);

    /* Clear the screen color to black */
    glClearColor(0.0,0.0,0.0,0.0);
    glEnableClientState(GL_VERTEX_ARRAY);
    glViewport (0, 0, (GLsizei) window_width, (GLsizei) window_height);
    /*
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);
    glDepthFunc(GL_LEQUAL);
    */
    glShadeModel (GL_SMOOTH);

    glCullFace(GL_BACK);
    glFrontFace(GL_CCW);
    glEnable(GL_CULL_FACE);
    //glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
    //glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);

    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glEnable(GL_LIGHT1);
    glEnable(GL_LIGHT2);
    glEnable(GL_LIGHT3);
    glEnable(GL_LIGHT4);
    glEnable(GL_DEPTH_TEST);

    initRotateCoords();
    initSpheres();
    glutSetKeyRepeat(GLUT_KEY_REPEAT_OFF);

    /* Set glut function calls */
    glutDisplayFunc(display);
    glutIdleFunc(display);
    glutReshapeFunc(reshape);
    glutKeyboardFunc(keyboardAction);
    glutKeyboardUpFunc(keyboardUp);
    glutMouseFunc(mouseClick);
    glutMotionFunc(mouseMotion);
    #ifdef FULLSCREEN
        glutFullScreen();
    #endif

    fmodchn->setPaused(false);

    // start the main program!
    redraw = true;
    // initial trajectories

    for (int j=0; j<INIT_NUM_SPHERES; j++) {
        generateTrajectory(j);
    }
    glutMainLoop();

    fmodsnd->release();
    fmodsys->close();
    fmodsys->release(); 
    return 0;   /* ISO C requires main to return int. */
}

void dumpSphereTarget(int i) {
    fprintf(stderr, "tar: x:%f ||| y:%f ||| z:%f \n"
        , sph_target[i][0]
        , sph_target[i][1]
        , sph_target[i][2]
        );
}

void dumpSpherePos(int i) {
    fprintf(stderr, "pos: x:%f ||| y:%f ||| z:%f \n"
        , sph_current[i][0]
        , sph_current[i][1]
        , sph_current[i][2]
        );
}

void initSpheres() {
    double radius, angle1, angle2;
    for (int i=0; i<INIT_NUM_SPHERES; i++) {
        radius = 50 + rand() % 30;
        angle1 = (rand() % 360) * PI / 180 ;
        angle2 = (rand() % 360) * PI / 180 ;
        sph_current[i][0] = radius * sin(angle1) * cos(angle2);
        sph_current[i][1] = radius * cos(angle1);
        sph_current[i][2] = radius * sin(angle1) * sin(angle2);
        sph_color[i][0] = 0.2 + (rand() % 5000) / 10000.0;
        sph_color[i][1] = 0.2 + (rand() % 5000) / 10000.0;
        sph_color[i][2] = 0.2 + (rand() % 5000) / 10000.0;
    }
}

void moveSphere(int i) {
    for (int j=0; j<3; j++) {
        sph_current[i][j] = sph_coeff[i][j][0] * pow(global_time, 3)
                        + sph_coeff[i][j][1] * global_time * global_time
                        + sph_coeff[i][j][2] * global_time
                        + sph_coeff[i][j][3];
    }
}

void generateTrajectory(int i) {
    double
        radius = 0
      , angle1 = 0, angle2 = 0
      , slope
      , lastslope
        ;
    /* select target */
    if (specL[i] || specR[i]) {
        radius = TRAJECTORY_RADIUS_MIN + specL[i] * TRAJECTORY_RADIUS_COEFF +  rand() % TRAJECTORY_RADIUS_RAND;
        angle1 = (rand() % 360 + specL[i] * 30) * PI / 180 ;
        angle2 = (rand() % 360 + specL[i] * 30) * PI / 180 ;
        slope = TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE;
        sph_target[i][0] = sph_current[i][0] + radius * sin(angle1) * cos(angle2);
        sph_target[i][1] = sph_current[i][1] + radius * cos(angle1);
        sph_target[i][2] = sph_current[i][2] + radius * sin(angle1) * sin(angle2);
        for(int j=0; j<3; j++) {
            while (sph_target[i][j] < sph_boundary[j][0])
                sph_target[i][j] += radius;
            while (sph_target[i][j] > sph_boundary[j][1])
                sph_target[i][j] -= radius;
        }
    }
    else {
        sph_target[i][0] = sph_current[i][0];
        sph_target[i][1] = sph_current[i][1];
        sph_target[i][2] = sph_current[i][2];
    }

    /* calculate cubic poly coeffs */
    for (int j=0; j<3; j++) {
        lastslope = sph_slope[i][j];
        sph_slope[i][j] = slope;
        sph_coeff[i][j][0] = sph_slope[i][j] + lastslope + 2 * sph_current[i][j] - 2 * sph_target[i][j];
        sph_coeff[i][j][1] = -sph_slope[i][j] - 2 * lastslope - 3 * sph_current[i][j] + 3 * sph_target[i][j];
        sph_coeff[i][j][2] = lastslope;
        sph_coeff[i][j][3] = sph_current[i][j];
    }
}
